package com.gce;

import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.checksum.CRC16;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.ReflectUtil;
import com.feilong.lib.javassist.bytecode.ClassFile;
import com.feilong.lib.javassist.bytecode.FieldInfo;
import com.feilong.lib.javassist.bytecode.MethodInfo;
import com.gce.model.ClazzInfoDefinition;
import com.gce.model.ClazzJarInfo;
import com.google.common.collect.Lists;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
import org.apache.maven.shared.artifact.filter.StrictPatternIncludesArtifactFilter;
import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;

import java.io.DataInputStream;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

/**
 *
 * https://shengulong.github.io/blog/2019/07/23/maven%E6%8F%92%E4%BB%B6%E8%B0%83%E8%AF%95%E6%96%B9%E6%B3%95/
 * @Author chengen.gce
 * @DATE 2022/1/17 9:14 下午
 */
@Mojo(name = "sayHi" ,requiresDependencyResolution = ResolutionScope.TEST)
public class HelloPlugin extends AbstractMojo {


    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    private MavenProject project;

    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    private MavenSession session;

    @Parameter( property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
    private String outputEncoding;

    /**
     * Contains the full list of projects in the reactor.
     */
    @Parameter( defaultValue = "${reactorProjects}", readonly = true, required = true )
    private List<MavenProject> reactorProjects;

    @Component
    private RepositorySystem repositorySystem;

    @Parameter ( defaultValue = "${repositorySystem}" )
    RepositorySystem repositorySystemParam;

    /**
     * The current repository/network configuration of Maven.
     */
    @Parameter( defaultValue = "${repositorySystemSession}" )
    private RepositorySystemSession repoSession;

    /**
     * The project's remote repositories to use for the resolution of project dependencies.
     */
    @Parameter( defaultValue = "${project.remoteProjectRepositories}" )
    private List<RemoteRepository> projectRepos;

    /**
     * The dependency collector builder to use.
     */
    @Component( hint = "default" )
    private DependencyCollectorBuilder dependencyCollectorBuilder;

    /**
     * The dependency graph builder to use.
     */
    @Component( hint = "default" )
    private DependencyGraphBuilder dependencyGraphBuilder;

    /**
     * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of
     * writing to the console.
     *
     * @since 2.0-alpha-5
     */
    @Parameter( property = "outputFile" )
    private File outputFile;

    /**
     * If specified, this parameter will cause the dependency tree to be written using the specified format. Currently
     * supported format are: <code>text</code> (default), <code>dot</code>, <code>graphml</code> and <code>tgf</code>.
     * These additional formats can be plotted to image files.
     *
     * @since 2.2
     */
    @Parameter( property = "outputType", defaultValue = "text" )
    private String outputType;

    /**
     * The scope to filter by when resolving the dependency tree, or <code>null</code> to include dependencies from all
     * scopes. Note that this feature does not currently work due to MSHARED-4
     *
     * @see <a href="https://issues.apache.org/jira/browse/MSHARED-4">MSHARED-4</a>
     * @since 2.0-alpha-5
     */
    @Parameter( property = "scope" )
    private String scope;

    /**
     * Whether to include omitted nodes in the serialized dependency tree. Notice this feature actually uses Maven 2
     * algorithm and <a href="https://maven.apache.org/shared/maven-dependency-tree/">may give wrong results when used
     * with Maven 3</a>.
     *
     * @since 2.0-alpha-6
     */
    @Parameter( property = "verbose", defaultValue = "false" )
    private boolean verbose;

    /**
     * The token set name to use when outputting the dependency tree. Possible values are <code>whitespace</code>,
     * <code>standard</code> or <code>extended</code>, which use whitespace, standard (ie ASCII) or extended character
     * sets respectively.
     *
     * @since 2.0-alpha-6
     */
    @Parameter( property = "tokens", defaultValue = "standard" )
    private String tokens;

    /**
     * A comma-separated list of artifacts to filter the serialized dependency tree by, or <code>null</code> not to
     * filter the dependency tree. The filter syntax is:
     *
     * <pre>
     * [groupId]:[artifactId]:[type]:[version]
     * </pre>
     *
     * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
     * segment is treated as an implicit wildcard.
     * <p>
     * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
     * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
     * </p>
     *
     * @see StrictPatternIncludesArtifactFilter
     * @since 2.0-alpha-6
     */
    @Parameter( property = "includes" )
    private String includes;

    /**
     * A comma-separated list of artifacts to filter from the serialized dependency tree, or <code>null</code> not to
     * filter any artifacts from the dependency tree. The filter syntax is:
     *
     * <pre>
     * [groupId]:[artifactId]:[type]:[version]
     * </pre>
     *
     * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
     * segment is treated as an implicit wildcard.
     * <p>
     * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
     * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
     * </p>
     *
     * @see StrictPatternExcludesArtifactFilter
     * @since 2.0-alpha-6
     */
    @Parameter( property = "excludes" )
    private String excludes;

    /**
     * The computed dependency tree root node of the Maven project.
     */
    private DependencyNode rootNode;

    /**
     * Whether to append outputs into the output file or overwrite it.
     *
     * @since 2.2
     */
    @Parameter( property = "appendOutput", defaultValue = "false" )
    private boolean appendOutput;

    /**
     * Skip plugin execution completely.
     *
     * @since 2.7
     */
    @Parameter( property = "skip", defaultValue = "false" )
    private boolean skip;

    private final static String KLASS_SUFFIX = "class";
    // Mojo methods -----------------------------------------------------------

    /*
     * @see org.apache.maven.plugin.Mojo#execute()
     */
    @Override
    public void execute()
            throws MojoExecutionException, MojoFailureException
    {
        if ( isSkip() )
        {
            getLog().info( "Skipping plugin execution" );
            return;
        }

        try {

            Set<Artifact> list = project.getArtifacts();
            printConfitClass(list);

        }

        catch ( Exception exception ) {
            throw new MojoExecutionException( "Cannot serialize project dependency graph", exception );
        }
    }

    // public methods ---------------------------------------------------------

    /**
     * Gets the Maven project used by this mojo.
     *
     * @return the Maven project
     */
    public MavenProject getProject()
    {
        return project;
    }

    /**
     * Gets the computed dependency graph root node for the Maven project.
     *
     * @return the dependency tree root node
     */
    public DependencyNode getDependencyGraph()
    {
        return rootNode;
    }

    /**
     * @return {@link #skip}
     */
    public boolean isSkip()
    {
        return skip;
    }

    /**
     * @param skip {@link #skip}
     */
    public void setSkip( boolean skip )
    {
        this.skip = skip;
    }

    /**
     * 找到冲突类 该类存在多个地方，且属性或方法有不同
     */
    private void findConfictClass(Set<Artifact> list) {

        Map<String,Set<ClazzJarInfo>> clzzMap = new ConcurrentHashMap<>(1000);
        ///// 解析jar 获取jar中的类信息 按类名称对jar组合
        list.parallelStream().forEach( artifact -> {
            Set<ClazzJarInfo> jarClazzSet = getClassInfoByJar(artifact.getFile());
            if (jarClazzSet.isEmpty()){
                return ;
            }
            jarClazzSet.parallelStream().forEach( clazzJarInfo -> {

                Set<ClazzJarInfo> classSet = clzzMap
                        .computeIfAbsent(clazzJarInfo.getClazzName(),(key) -> new ConcurrentHashSet<>());
                classSet.add(clazzJarInfo);
            });

        });

        /////相同类出现多个jar时 需要做相应的排除算法
        clzzMap.entrySet().forEach( entry -> {
            if (entry.getValue().size() == 1) {
                return ;
            }

            Set<ClazzJarInfo> values = entry.getValue();

            ////排除CRC相同的类
            Set<String> crcSet = new HashSet<>();
            values.forEach( cls -> {
                crcSet.add(cls.getCrc());
            });
            if (crcSet.size() ==1){
                return ;
            }
            ///// CRC不相等的情况 对类的属性方法拼装成string
            Set<String> fieldMethodSet = new ConcurrentHashSet<>();
            values.forEach(cls -> {

                String fieldMethod = rubFiledMethod(cls);
                fieldMethodSet.add(fieldMethod);
            });
            if (fieldMethodSet.size() == 1){
                return ;
            }
            for (ClazzJarInfo clazzJarInfo : values) {

                getLog().warn("class: " + clazzJarInfo.getClazzName() + " CRC("
                        + clazzJarInfo.getCrc() + ") jarName : " + clazzJarInfo.getJarName()
                        + " " + clazzJarInfo.getFiledStr() + " " + clazzJarInfo.getMethodStr());
            }

        });
    }

    public String rubFiledMethod(ClazzJarInfo jarInfo) {

        Class<?> clazz = jarInfo.getClazz();
        ///获取类属性
        List<String> sorFieldtList = new ArrayList<>();
        Field[] fields = ReflectUtil.getFields(clazz);
        if (fields != null){
            for (Field field : fields){
                sorFieldtList.add(field.getName());
            }
        }
        /////获取类方法
        List<String> sortMethodList = new ArrayList<>();
        Method[] methods = ReflectUtil.getMethods(clazz);
        if (methods != null){
            for (Method method: methods){
                sortMethodList.add(method.getName() + "|" + String.valueOf(method.getParameterCount()) );
            }
        }
        Collections.sort(sorFieldtList);
        Collections.sort(sortMethodList);

        StringBuilder sb = new StringBuilder();
        sb.append("##FIELD##");
        for (String s : sorFieldtList) {
            sb.append(s).append("-");
        }
        jarInfo.setFiledStr(sb.toString());
        sb = new StringBuilder();
        sb.append("##METHOD##");
        for (String s : sortMethodList) {
            sb.append(s).append("-");
        }
        jarInfo.setMethodStr(sb.toString());
        return sb.toString();
    }

    private Set<ClazzJarInfo> getClassInfoByJar(File file) {

        Set<ClazzJarInfo> classSet = new HashSet<>();
        try {
            URL url = file.toURI().toURL();

            ////加载外部类使用 URLClassLoader
            URLClassLoader classLoader = getURLClassLoader(url);
            JarFile jarFile = new JarFile(file.getAbsolutePath());
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String entryName = jarEntry.getName();

                if (entryName.endsWith(KLASS_SUFFIX)) {
                    try{
                        entryName = entryName.replace(".class", "").replaceAll("/", ".");

                        InputStream in = classLoader.getResourceAsStream(jarEntry.getName());
                        String crc16 = createCRC(in);
                        ClazzJarInfo clazzJarInfo = new ClazzJarInfo();

                        Class<?> clazz = classLoader.loadClass(entryName);

                        clazzJarInfo.setClazz(clazz);
                        clazzJarInfo.setCrc(crc16);
                        clazzJarInfo.setJarName(jarFile.getName());
                        clazzJarInfo.setClazzName(entryName);
                        classSet.add(clazzJarInfo);
                    } catch (Throwable e) {
                        getLog().error("-------->" + e.getMessage() + "######"+ jarFile.getName());
                    }

                }
            }
        } catch (Exception e) {

        }
        return classSet;
    }

    public void printConfitClass(Set<Artifact> list ){

        String localMavenRepository =  project.getProjectBuildingRequest().getLocalRepository().getBasedir();
        getLog().info("local maven repository : " + localMavenRepository);
        getLog().info("search class conflict detect start...");


        Map<String,Set<ClazzJarInfo>> clazzMap = new ConcurrentHashMap<>(100);
        list.forEach(artifact -> {
            try{
                File file = artifact.getFile();
                Set<ClazzJarInfo> subClazzSet = getClassNameByJar(file);

                if (subClazzSet.isEmpty()){
                    return ;
                }

                subClazzSet.forEach( clazzInfo -> {

                    Set<ClazzJarInfo> set = clazzMap
                            .computeIfAbsent(clazzInfo.getClazzName(),(key ) -> new ConcurrentHashSet<>());
                    set.add(clazzInfo);
                });

            }catch (Exception e) {
                getLog().error("search class exception: " ,e);
            }
        });



        List<ClazzJarInfo> equalCRCList = new CopyOnWriteArrayList<>();
        List<ClazzJarInfo> unEqualCRCList = new CopyOnWriteArrayList<>();

        /////检测同一个类中是否有多个jar 并且内容是否不一样
        getLog().warn("unequal class conflict detect start " + unEqualCRCList.size() +"...");
        clazzMap.entrySet().stream().forEach( entry -> {

            Map<String ,List<ClazzJarInfo>> map = new HashMap<>();
            List<ClazzJarInfo> sameClassInDiffentJarList = new ArrayList<>();
            Set<String> allFieldSet = new HashSet<>();
            Set<String> allMethodSet = new HashSet<>();
            boolean unEqualBoolean = false;
            for (ClazzJarInfo clazzJarInfo : entry.getValue()) {
                List<ClazzJarInfo> inList = map.computeIfAbsent(clazzJarInfo.getCrc(),(key) -> new ArrayList<>());
                inList.add(clazzJarInfo);
                sameClassInDiffentJarList.add(clazzJarInfo);
                allFieldSet.addAll(clazzJarInfo.getFieldSet());
                allMethodSet.addAll(clazzJarInfo.getMethodSet());
                if (map.size() > 1) {
                    unEqualBoolean = true;
                }
            }

            if (unEqualBoolean) {

                for (Map.Entry<String, List<ClazzJarInfo>> stringListEntry : map.entrySet()) {
                    unEqualCRCList.addAll(stringListEntry.getValue());
                }
                System.out.println("-------------------[" + sameClassInDiffentJarList.get(0).getClazzName() + "]-----------------------");
                sameClassInDiffentJarList.sort(Comparator.comparing(ClazzJarInfo::getCrc));
                for (ClazzJarInfo clazzJarInfo : sameClassInDiffentJarList){
                    Set<String> clazzFieldSet = clazzJarInfo.getFieldSet();
                    Set<String> clazzMethodSet = clazzJarInfo.getMethodSet();
                    List<String> diffFieldList = allFieldSet.parallelStream().filter( str -> !clazzFieldSet.contains(str)).collect(Collectors.toList());
                    List<String> diffMethodList = allMethodSet.parallelStream().filter( str -> !clazzMethodSet.contains(str)).collect(Collectors.toList());
                    getLog().warn("class: " + clazzJarInfo.getClazzName() + " CRC(" + clazzJarInfo.getCrc()
                            + ") --" + clazzJarInfo.getJarName() + " \n    缺少 field: " + diffFieldList
                            + " ; \n    缺少method: " + diffMethodList
                            + " ; \n    jarName  : [" + clazzJarInfo.getJarName()
                    + "] \n    ##FIELD-METHOD##" + clazzJarInfo.getFieldMethodNames());
                }


            } else {
                for (Map.Entry<String, List<ClazzJarInfo>> stringListEntry : map.entrySet()) {
                    equalCRCList.addAll(stringListEntry.getValue());
                }
            }

        });
        getLog().warn("unequal class conflict detect end..."  );

        getLog().info("class conflict detect end... size: " + (unEqualCRCList.size() + equalCRCList.size())  );
    }


    private Set<ClazzJarInfo> getClassNameByJar(File file) {

        Set<ClazzJarInfo> classSet = new HashSet<>();
        try {
            URL url = file.toURI().toURL();

            ////加载外部类使用 URLClassLoader
            ClassLoader classLoader = getURLClassLoader(url);
            JarFile jarFile = new JarFile(file.getAbsolutePath());
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String entryName = jarEntry.getName();

                if (entryName.endsWith(KLASS_SUFFIX)) {

                    /////java 9 后的模块文件去掉
                    if (entryName.endsWith("module-info.class")){
                        continue;
                    }
                    entryName = entryName.replace(".class", "").replaceAll("/", ".");
                    InputStream in = classLoader.getResourceAsStream(jarEntry.getName());
                    ////解析出类属性方法;
                    try{
                        //ClazzInfoDefinition clazzInfoDefinition = ClassReader.read(in);
                        DataInputStream dataInputStream = new DataInputStream(in);

                        ClassFile classFile = new ClassFile(dataInputStream);
                        List<FieldInfo>  fieldInfos = classFile.getFields();
                        List<MethodInfo> methodInfos = classFile.getMethods();

                        ClazzInfoDefinition clazzInfoDefinition = new ClazzInfoDefinition();
                        clazzInfoDefinition.setClassname(classFile.getName());
                        clazzInfoDefinition.setSuperClassname(classFile.getSuperclass());
                        clazzInfoDefinition.getInterfaceNameList().addAll(Lists.newArrayList(classFile.getInterfaces()));
                        clazzInfoDefinition.getMethodInfoList().addAll(methodInfos);
                        clazzInfoDefinition.getFieldInfoList().addAll(fieldInfos);

                        ////将类信息
                        String splitClassInfo = clazzInfoDefinition.splitSortClassInfo();

                        String crc16 = createCRC(splitClassInfo.getBytes(StandardCharsets.UTF_8));
                        ClazzJarInfo clazzJarInfo = new ClazzJarInfo();

                        clazzJarInfo.setJarName(file.getName());
                        clazzJarInfo.setClazzName(entryName);
                        clazzJarInfo.setCrc(crc16);
                        clazzJarInfo.setFieldMethodNames(splitClassInfo);
                        clazzJarInfo.getFieldSet().addAll(clazzInfoDefinition.getFieldSet());
                        clazzJarInfo.getMethodSet().addAll(clazzInfoDefinition.getMethodSet());
                        classSet.add(clazzJarInfo);
                    }catch (Exception e){
                        getLog().error("异常-->"+e.getMessage() + " jar:"
                                + file.getAbsolutePath() + " ; class : " + entryName);
                    }

                }
            }
        } catch (Exception e) {

        }
        return classSet;
    }

    public static String createCRC(byte[] bytes ) {
        CRC16 crc = new CRC16();
        crc.update(bytes, 0, bytes.length);
        return HexUtil.toHex(crc.getValue());
    }


    public static String createCRC(InputStream in ) {
        byte[] bytes = IoUtil.readBytes(in);
        CRC16 crc = new CRC16();
        crc.update(bytes, 0, bytes.length);
        return HexUtil.toHex(crc.getValue());
    }




    private static URLClassLoader getURLClassLoader(URL ...urls) {

        return new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
    }

}
